iOS Socket通讯 - Socket C/S实例

iOS提供的Socket网络编程的接口有CFSocket,BSD Socket,比较著名的第三方库是AsyncSocket。

  • BSD Socket 是UNIX系统中通用的网络接口,它不仅支持各种不同的网络类型,而且也是一种内部进程之间的通信机制。而iOS系统其实本质就是UNIX,所以可以用,但是比较复杂。
  • CFSocket 是苹果提供给我们的使用Socket的方式。
  • AsyncSocket 是一个应用比较广泛的开源库。

下面我们用代码来演示如何使用AsyncSocket来创建一个服务器和客户端。

AsyncSocket全名是CocoaAsyncSocket,它的GitHub地址点此访问。AsyncSocket使用CocoaPods安装和拖放安装有所不同。CocoaPods上的版本只有GCDAsyncSocket,GItHub上多了RunloopAsyncSocket,而后者现在已经弃用,建议直接使用CocoaPods安装。

缺点是onsocketWillDisconnect这个代理方法在GCD的版本里暂时没有,也没有找到替代方法。

下面开始使用AsyncSocket编写服务器端的程序,首先说一下要实现的目的:

  1. 用户输入昵称,将昵称发送给服务器,服务器返回在线名单;
  2. 客户端收到在线名单,利用在线名单生成联系人列表;
  3. 点击联系人列表,用户和服务器进行对话;

下面开始进入正题:

Server

这里创建的是Mac的Application,首先创建:

Socket单例类

1
2
3
4
5
6
7
8
9
class MySocket: GCDAsyncSocketDelegate {
private var listener: GCDAsyncSocket!
static let sharedSocket = MySocket()
private init() {
let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
listener = GCDAsyncSocket(delegate: self,delegateQueue: globalQueue)
listener.delegate = self
}
}

添加3个属性,创建监听的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var isRuning: Bool = false
var port: UInt16!
var host: String!
func startListen() {
if !isRuning {
do {
try listener.acceptOnPort(port)
print("开始监听 \(host) \(port)")
isRuning = true
}
catch {
print("服务开启失败")
}
}
else {
print("重新监听")
listener.disconnect()
for connectionSocket in connectionSockets {
connectionSocket.disconnect()
}
isRuning = false
}
}

添加一个属性,创建发送方法:

1
2
3
4
5
var currentSocket: GCDAsyncSocket!
func sendMessage(message: NSData) {
currentSocket.writeData(message, withTimeout: -1, tag: 0)
print("发送消息")
}

添加发送消息成功和连接成功两个代理方法:

1
2
3
4
5
6
7
8
9
10
11
var connectionSockets: [GCDAsyncSocket] = []
@objc func socket(sock: GCDAsyncSocket, didWriteDataWithTag tag: Int) {
print("发送消息成功之后回调")
listener.readDataWithTimeout(-1, tag: tag)
}
@objc func socket(sock: GCDAsyncSocket, didAcceptNewSocket newSocket: GCDAsyncSocket) {
newSocket.readDataWithTimeout(-1, tag: 0)
connectionSockets.append(newSocket)
print("收到新的socket")
print(sock.userData)
}

添加收到消息的代理事件方法,并处理收到的消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var currentHost: String!
var recivieMessage: (String -> ())!
var userStates: [[String!]] = []
@objc func socket(sock: GCDAsyncSocket, didReadData data: NSData, withTag tag: Int) {
let msg = String(data: data, encoding: NSUTF8StringEncoding)
recivieMessage(msg!)
let tmpMsg = msg?.componentsSeparatedByString(":")
if tmpMsg?.first == "user" {
do {
currentHost = "\(sock.connectedHost!)"
let tmpArr = [currentHost, "\(tmpMsg![1])", "1"]
userStates.append(tmpArr)
let json = try NSJSONSerialization.dataWithJSONObject(userStates, options: .PrettyPrinted)
sock.writeData(json, withTimeout: -1, tag: 0)
}
catch {
print("传输出错")
}
}
sock.readDataWithTimeout(-1, tag: 0)
currentSocket = sock
}

添加断开连接的代理事件方法:

1
2
3
4
5
6
7
8
9
10
@objc func socketDidDisconnect(sock: GCDAsyncSocket, withError err: NSError?) {
print("socket连接已经关闭 \(err)")
for i in 0..<userStates.count {
if userStates[i][0] == currentHost {
userStates[i][2] = "0"
let msg = "\(userStates[i][1])已下线"
sendMessages(msg, sockets: connectionSockets)
}
}
}

此时服务器的Socket封装已经做好了,下面是:

Server UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class ViewController: NSViewController, GCDAsyncSocketDelegate {
@IBOutlet weak var messageField: NSTextField!
@IBOutlet weak var receiveField: NSTextField!
var socket: MySocket!
override func viewDidLoad() {
super.viewDidLoad()
socket = MySocket.sharedSocket
socket.port = 1234
socket.host = "127.0.0.1"
socket.startListen()
socket.recivieMessage = { msg in
self.receiveField.stringValue = msg
}
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
@IBAction func sendButtonDidTouch(sender: AnyObject) {
if messageField.stringValue.isEmpty {
receiveField.stringValue = "请输入消息"
}
else {
socket.sendMessage(("msg:" + messageField.stringValue).dataUsingEncoding(NSUTF8StringEncoding)!)
messageField.stringValue = ""
}
}
}

下面是客户端的制作,这里是iOS的Application:

Client

同样首先封装一个Socket单例类:

Socket单例类

1
2
3
4
5
6
7
8
9
10
11
12
class MySocket: GCDAsyncSocketDelegate {
var port: UInt16!
var host: String!
private var listener: GCDAsyncSocket!
static let sharedSocket = MySocket()
private init() {
let globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
listener = GCDAsyncSocket(delegate: self,delegateQueue: globalQueue)
}
}

客户端和服务器的区别是不需要监听,但需要连接,所以创建连接服务器的方法:

1
2
3
4
5
6
7
8
func connectToServer() {
do {
try listener.connectToHost(host, onPort: port)
}
catch {
print("连接服务器出错")
}
}

创建消息发送的方法:

1
2
3
func sendMessage(message: NSData) {
listener.writeData(message, withTimeout: -1, tag: 0)
}

添加消息发送成功后的代理方法:

1
2
3
4
@objc func socket(sock: GCDAsyncSocket, didWriteDataWithTag tag: Int) {
print("发送消息成功之后回调")
listener.readDataWithTimeout(-1, tag: tag)
}

创建两个闭包,添加接收消息的代理方法,并处理接收的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var receiveMessage: (String -> ())!
var receuveUserStates: (NSArray -> Void)!
@objc func socket(sock: GCDAsyncSocket, didReadData data: NSData, withTag tag: Int) {
let receive = String(data: data, encoding: NSUTF8StringEncoding)
let tmpMsg = receive?.componentsSeparatedByString(":")
if tmpMsg?.first == "msg" {
receiveMessage(receive!)
}
else {
do {
let receive = try NSJSONSerialization.JSONObjectWithData(data, options: .MutableLeaves) as! NSArray
receuveUserStates(receive)
}
catch {
print("读取Json出错")
}
}
sock.readDataWithTimeout(-1, tag: 0)
}

这样客户端的Socket就封装好了,下面是客户端的UI部分:

Client UI

客户端的UI比较复杂,分为3层,第一层是用户输入自己的昵称,将昵称发送到服务器,服务器返回在线名单,利用在线名单进入下一层。

第一层

搭建UI,点击发送时连接服务器并发送格式消息,利用闭包将传回的数据并打开第二层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class MainVC: UIViewController {
@IBOutlet weak var nicenameField: UITextField!
var userStates: NSArray!
override func viewDidLoad() {
super.viewDidLoad()
MySocket.sharedSocket.receuveUserStates = { arr in
self.userStates = arr
let tableVC = TableVC()
print(self.userStates)
tableVC.userStates = self.userStates
dispatch_async(dispatch_get_main_queue(), {
self.presentViewController(tableVC, animated: true, completion: nil)
})
print("闭包userStates")
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
@IBAction func submitButtonClicked(sender: AnyObject) {
if nicenameField.text == "" {
let alert = UIAlertController(title: "提示", message: "请输入昵称", preferredStyle: .Alert)
let action = UIAlertAction(title: "好的", style: .Default, handler: { (action) in
print("提示框被点击")
})
alert.addAction(action)
self.presentViewController(alert, animated: true, completion: nil)
}
else {
let socket = MySocket.sharedSocket
socket.host = "127.0.0.1"
socket.port = 1234
socket.connectToServer()
socket.sendMessage("user:\(nicenameField.text!)".dataUsingEncoding(NSUTF8StringEncoding)!)
}
}
}

第二层

利用收到的在线名单,创建TableView列表,点击列表项时打开第三层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class TableVC: UITableViewController {
var nickname: String!
var myNavigationBar: UINavigationBar!
var userStates: NSArray!
override func viewDidLoad() {
super.viewDidLoad()
installUI()
print(userStates)
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 44
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return userStates.count
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print(indexPath.row)
let ioVC = IOVC()
let tmpArr = userStates[indexPath.row] as! NSArray
ioVC.nickname = tmpArr[1] as! String
self.presentViewController(ioVC, animated: true, completion: nil)
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("TableCell", forIndexPath: indexPath) as! TableCell
let tmpArr = userStates[indexPath.row] as! NSArray
cell.nicknameLabel.text = (tmpArr[1] as? String)!
print(tmpArr[1] as? String)
if tmpArr[2] as! String == "1" {
cell.stateView.backgroundColor = UIColor.greenColor()
}
return cell
}
func installUI() {
let nib = UINib(nibName: "TableCell", bundle: nil)
self.tableView.registerNib(nib, forCellReuseIdentifier: "TableCell")
let screenRect = UIScreen.mainScreen().bounds
myNavigationBar = UINavigationBar(frame: CGRectMake(0, 0, screenRect.size.width, 64))
myNavigationBar.tintColor = UIColor.redColor()
let navigationTitleItem = UINavigationItem(title: "当前在线")
myNavigationBar.pushNavigationItem(navigationTitleItem, animated: true)
myNavigationBar.barStyle = .Black
self.tableView.tableHeaderView = myNavigationBar
}
}

第三层

点击发送对话到服务器,利用闭包将服务器返回的消息显示在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class IOVC: UIViewController {
var nickname: String!
var messageField: UITextField!
var submitButton: UIButton!
var receiveView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
installUI(nickname)
MySocket.sharedSocket.receiveMessage = { receive in
dispatch_async(dispatch_get_main_queue(), {
self.receiveView.text = receive
})
}
}
func submitButtonDidTouch() {
if messageField.text == "" {
let alert = UIAlertController(title: "提示", message: "发送消息不能为空", preferredStyle: .Alert)
let action = UIAlertAction(title: "好的", style: .Default, handler: { (action) in
print("提示框被点击")
})
alert.addAction(action)
self.presentViewController(alert, animated: true, completion: nil)
}
else {
let message = ("\(nickname):"+messageField.text!).dataUsingEncoding(NSUTF8StringEncoding)
let socket = MySocket.sharedSocket
socket.sendMessage(message!)
messageField.text = ""
}
}
func installUI(nickname: String) {
self.view.backgroundColor = UIColor(red: 0/255, green: 204/255, blue: 102/255, alpha: 1)
let screenBouns = UIScreen.mainScreen().bounds
let navigationBar = UINavigationBar(frame: CGRect(x: 0, y: 0, width: screenBouns.size.width, height: 64))
let navigationTitle = UINavigationItem(title: nickname)
navigationBar.pushNavigationItem(navigationTitle, animated: true)
navigationBar.barStyle = .Black
self.view.addSubview(navigationBar)
messageField = UITextField(frame: CGRect(x: 0, y: 0, width: screenBouns.size.width, height: 30))
messageField.center = self.view.center
messageField.center.y = messageField.center.y + 30
messageField.borderStyle = .None
messageField.textAlignment = .Center
messageField.font = UIFont.systemFontOfSize(14)
messageField.textColor = UIColor.whiteColor()
self.view.addSubview(messageField)
let topCutLine = UIView(frame: CGRect(x: 20, y: 0, width: screenBouns.size.width - 40, height: 1))
topCutLine.backgroundColor = UIColor.whiteColor()
topCutLine.center = messageField.center
topCutLine.center.y = messageField.center.y - 20
self.view.addSubview(topCutLine)
let bottomCutLine = UIView(frame: CGRect(x: 20, y: 0, width: screenBouns.size.width - 40, height: 1))
bottomCutLine.backgroundColor = UIColor.whiteColor()
bottomCutLine.center = messageField.center
bottomCutLine.center.y = messageField.center.y + 20
self.view.addSubview(bottomCutLine)
submitButton = UIButton(frame: CGRect(x: 20, y: 0, width: screenBouns.size.width - 40, height: 44))
submitButton.backgroundColor = UIColor.darkGrayColor()
submitButton.layer.cornerRadius = 5
submitButton.setTitle("发送到服务器", forState: .Normal)
submitButton.titleLabel?.font = UIFont.systemFontOfSize(15)
submitButton.setTitleColor(UIColor.whiteColor(), forState: .Normal)
submitButton.center = self.view.center
submitButton.center.y = self.view.frame.size.height - 68
submitButton.addTarget(self, action: #selector(IOVC.submitButtonDidTouch), forControlEvents: .TouchUpInside)
self.view.addSubview(submitButton)
receiveView = UITextView(frame: CGRect(x: 20, y: 0, width: screenBouns.size.width - 40, height: 88))
receiveView.textAlignment = .Center
receiveView.backgroundColor = UIColor.clearColor()
receiveView.text = "等待回复"
receiveView.textColor = UIColor.whiteColor()
receiveView.font = UIFont.systemFontOfSize(14)
receiveView.center = self.view.center
receiveView.center.y = 120
self.view.addSubview(receiveView)
}
}

Demo下载请点击这里